package com.freedom.sql.jdbc.request;

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;

import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlShowCreateTableStatement;
import com.freedom.sql.jdbc.bind.Bind;
import com.freedom.sql.jdbc.bind.Link;
import com.freedom.sql.jdbc.netty.MyNioSocketChannel;
import com.freedom.sql.jdbc.pool.socket.MySocket;
import com.freedom.sql.jdbc.response.CommonOkPacket;
import com.freedom.sql.jdbc.utils.ByteUtils;
import com.freedom.sql.jdbc.utils.LoggerUtils;
import com.freedom.sql.jdbc.utils.MyException;
import com.freedom.sql.jdbc.utils.MyJsonProperties;
import com.freedom.sql.jdbc.utils.RandomUtils;
import com.freedom.sql.jdbc.utils.StringUtils;

public class ShowCreateTable {
	private static final Logger logger = LogManager.getLogger(ShowCreateTable.class);
	private MyNioSocketChannel channel;
	private MySqlShowCreateTableStatement statement;
	@SuppressWarnings("unused")
	private int packetSeq;
	private String originSQL;

	public ShowCreateTable(int _packetSeq, MyNioSocketChannel _channel, MySqlShowCreateTableStatement _statement,
			String _originSQL) {
		packetSeq = _packetSeq;
		channel = _channel;
		statement = _statement;
		originSQL = _originSQL;
	}

	public void read() throws Exception {
		// 开始处理,发送表格给client
		// 先判断库是否已经存在
		long begin = System.currentTimeMillis();
		String db = channel.getDb();
		if (null == db || db.length() <= 0) {
			throw new MyException("no db specified...", false);// 默认不关闭
		}
		// 提取表名
		String table = null;
		try {
			table = statement.getName().toString();
			LoggerUtils.debug(logger, "table:[" + table + "]");
		} catch (Exception e) {
			LoggerUtils.error(logger, e.toString());
			throw new Exception("fail to parse table form [" + originSQL + "]");
		}
		if (table == null) {
			throw new Exception("null table form [" + originSQL + "]");
		}
		table = table.trim();
		if (table.length() <= 0) {
			throw new Exception("empty table form [" + originSQL + "]");
		}
		// 如果是`Hello_测试表0`这种类型的，需要去掉两边的`,字符为0x60
		{
			byte[] tableBytes = ByteUtils.encode(table, channel.getCharset());
			int currentLength = tableBytes.length;
			if (0x60 == tableBytes[0]) {
				System.arraycopy(tableBytes, 1, tableBytes, 0, currentLength - 1);
				currentLength--;
			}
			if (0x60 == tableBytes[currentLength - 1]) {
				currentLength--;
			}
			byte[] newTableBytes = new byte[currentLength];
			System.arraycopy(tableBytes, 0, newTableBytes, 0, currentLength);
			table = ByteUtils.decode(newTableBytes, channel.getCharset());
		}
		LoggerUtils.debug(logger, "table:[" + table + "]");
		// 库和表都拿到了，开始转发
		// 从读表获取
		// 经过思考,决定从读表获取,遍历一遍
		HashMap<String, Bind> binds = MyJsonProperties.getInstance().getBinds();
		if (null == binds) {
			throw new Exception("no binds specified");
		}
		Bind bind = binds.get(StringUtils.union(db, table));
		if (null == bind) {
			throw new Exception("no bind found");
		}
		// 找到了bind的读数据源
		ArrayList<Link> links = bind.getReadList();
		if (null == links || links.size() == 0) {
			throw new Exception("no links found");
		}

		boolean succeed = false;
		// 选择几次做重试机制呢?
		for (int count = 2; count >= 0; count--) {
			// 取随机数
			int index = RandomUtils.randomInt(0, links.size() - 1);
			LoggerUtils.debug(logger, "random index: " + index);
			Link link = links.get(index);
			try {
				this.getResultBasedOnLink(link);
				succeed = true;
				break;
			} catch (Exception e) {
				LoggerUtils.error(logger, e.toString());
			}
		}
		// 判断是否成功
		long end = System.currentTimeMillis();
		LoggerUtils.debug(logger, "it costs " + (end - begin) + " ms");
		if (false == succeed) {
			throw new Exception(this.originSQL + "fail to get fields of [" + db + "][" + table + "]");
		}
		// 获取结束
	}

	public void getResultBasedOnLink(Link link) throws Exception {
		String dataSourceName = link.getName();
		GenericObjectPool<MySocket> pools = MyJsonProperties.getInstance().getDataSources().get(dataSourceName);
		if (null == pools) {
			throw new Exception("no commons pool found for " + dataSourceName);
		}
		LoggerUtils.debug(logger, "pools --- " + pools);
		// 找到了连接池，开始取连接
		MySocket mySocket = (MySocket) pools.borrowObject();
		if (null == mySocket) {
			LoggerUtils.error(logger, "no available socket in commons pool[" + dataSourceName + "]");
			throw new Exception("no available socket in commons pool[" + dataSourceName + "]");
		}
		LoggerUtils.debug(logger, "socket --- " + mySocket);
		// 现在拿到了一个有效的物理连接
		// 使用前，先设置一些属性
		// 每次都是使用方负责设置这些属性，不需要善后
		LoggerUtils.debug(logger, "channel---" + mySocket.getChannel());
		mySocket.getChannel().setSoTimeout(3000);// 重置超时时间
		LoggerUtils.info(logger, "big con! fetch valid socket now...!!!");
		try {
			byte[] allBytes;
			// 如果不是自动提交，则设置为自动提交
			if (0 == mySocket.getAutoCommit()) {
				new AutoCommitPacket(mySocket, 1).write();
				CommonOkPacket currentPacket = new CommonOkPacket(mySocket.getChannel()).read();
				allBytes = currentPacket.getAllBytes();
				if ((byte) 0xff == allBytes[0]) {
					LoggerUtils.error(logger, "set autocommit fail...");
					throw new Exception("set autocommit fail...");
				}
				mySocket.setAutoCommit(1);
				LoggerUtils.debug(logger, "SET autocommit=1 -> bingo!");
			}
			// 首先判断是否需要切库
			String physicalDB = link.getPhysicalDB();
			String physicalTable = link.getPhysicalTable();
			String oldPhysicalDB = mySocket.getPhysicalDB();
			boolean noNeedToChangeDB = (null != oldPhysicalDB && oldPhysicalDB.equals(physicalDB));
			if (false == noNeedToChangeDB) {
				new UseDatabasePacket(mySocket, physicalDB).write();
				CommonOkPacket commonPacket = new CommonOkPacket(mySocket.getChannel()).read();
				// 判断成功还是失败
				allBytes = commonPacket.getAllBytes();
				if (0 != allBytes[0]) {// 查看报文，use database的这个字段正常为0
					LoggerUtils.error(logger, "use database fail...");
					throw new Exception("use database fail...");
				}
				mySocket.setPhysicalDB(physicalDB);// 保留物理库,下次就不用再切了
				LoggerUtils.debug(logger, "use database " + physicalDB + " ok");
			} else {
				LoggerUtils.debug(logger, "no need to change db");
			}
			// 发送真正的命令
			LoggerUtils.debug(logger, "try to request " + physicalDB + " ok");
			new ShowCreateTable(mySocket, physicalTable).write();
			// 然后结果需要原封不动的返回给前端socket
			// 1)先读header-出错会抛出异常，所以不用担心
			CommonOkPacket currentPacket = new CommonOkPacket(mySocket.getChannel()).read();
			allBytes = currentPacket.getAllBytes();
			if ((byte) 0xff == allBytes[0]) {
				LoggerUtils.error(logger, "read header fail...");
				throw new Exception(this.originSQL + " read header for fields of table fail...");
			}
			this.channel.writeAndFlush(currentPacket.toByteBuf());
			LoggerUtils.debug(logger, "read header succeed.");
			// 2)继续读2个fields
			int fieldCount = ByteUtils.bigEndianInteger(currentPacket.getAllBytes(), 0,
					currentPacket.getAllBytes().length);
			LoggerUtils.debug(logger, "fieldCount : " + fieldCount);
			for (int index = 0; index < fieldCount; index++) {
				currentPacket = new CommonOkPacket(mySocket.getChannel()).read();
				this.channel.writeAndFlush(currentPacket.toByteBuf());
				LoggerUtils.debug(logger, this.originSQL + " read field succeed.");
			}
			// 3)读第一个EOF
			currentPacket = new CommonOkPacket(mySocket.getChannel()).read();
			this.channel.writeAndFlush(currentPacket.toByteBuf());
			LoggerUtils.debug(logger, this.originSQL + " read first eof succeed.");
			// 4)读真正的内容
			currentPacket = new CommonOkPacket(mySocket.getChannel()).read();
			this.channel.writeAndFlush(currentPacket.toByteBuf());
			LoggerUtils.debug(logger, this.originSQL + " read real content succeed.");
			// 5)读最后的EOF
			currentPacket = new CommonOkPacket(mySocket.getChannel()).read();
			this.channel.writeAndFlush(currentPacket.toByteBuf());
			LoggerUtils.debug(logger, "read last eof succeed.");
			// 全部结束，恭喜
		} catch (Exception e) {
			mySocket.setException(true);// 保留业务侧的异常情况,此socket会被连接池驱逐
			throw e;
		} finally {// 务必要归还
			pools.returnObject(mySocket);
			LoggerUtils.debug(logger, "relase socket to pool");
		}
	}

	// write
	private MySocket writeSocket;
	private String table;

	public ShowCreateTable(MySocket _s, String _table) {
		this.writeSocket = _s;
		this.table = _table;
	}

	public void write() throws Exception {
		OutputStream out = writeSocket.getChannel().getOutputStream();
		byte[] headerLen = null;
		byte[] seq = new byte[] { 0x00 };
		byte[] type = new byte[] { 0x03 };
		byte[] prefix = "SHOW CREATE TABLE ".getBytes();
		byte[] tableBytes = ByteUtils.encode(this.table, writeSocket.getCharset());
		headerLen = ByteUtils.writeInt(1 + prefix.length + tableBytes.length, 3);
		out.write(headerLen);
		out.write(seq);
		out.write(type);
		out.write(prefix);
		out.write(tableBytes);
		out.flush();
	}
}
